ひとりNavigation API Advent Calendar 18日目
https://gyazo.com/4b48de584c015b46f603f5678a835b8a
yamanoku.icon 面白そうなことをやってそうに見える
<a>要素でやっている
code:packages/kit/src/runtime/client/constants.js
export const SNAPSHOT_KEY = 'sveltekit:snapshot';
export const SCROLL_KEY = 'sveltekit:scroll';
export const STATES_KEY = 'sveltekit:states';
export const PAGE_URL_KEY = 'sveltekit:pageurl';
export const HISTORY_INDEX = 'sveltekit:history';
export const NAVIGATION_INDEX = 'sveltekit:navigation';
code:packages/kit/src/runtime/client/client.js
if (!current_history_index) {
// we use Date.now() as an offset so that cross-document navigations
// within the app don't result in data loss
current_history_index = current_navigation_index = Date.now();
// create initial history entry, so we can return here
history.replaceState(
{
...history.state,
},
''
);
}
初期化設定
code:packages/kit/src/runtime/client/client.js
if (!popped) {
// this is a new navigation, rather than a popstate
const change = replace_state ? 0 : 1;
const entry = {
};
const fn = replace_state ? history.replaceState : history.pushState;
fn.call(history, entry, '', url);
if (!replace_state) {
clear_onward_history(current_history_index, current_navigation_index);
}
}
pushStateとreplaceStateの切り分け
code:packages/kit/src/runtime/client/client.js
addEventListener('popstate', async (event) => {
if (resetting_focus) return;
token = {};
// if a popstate-driven navigation is cancelled, we need to counteract it
// with history.go, which means we end up back here, hence this check
if (history_index === current_history_index) return;
const url = new URL(event.statePAGE_URL_KEY ?? location.href); const is_hash_change = current.url ? strip_hash(location) === strip_hash(current.url) : false;
const shallow =
navigation_index === current_navigation_index && (has_navigated || is_hash_change);
if (shallow) {
// We don't need to navigate, we just need to update scroll and/or state.
// This happens with hash links and pushState/replaceState. The
// exception is if we haven't navigated yet, since we could have
// got here after a modal navigation then a reload
if (state !== page.state) {
page.state = state;
}
update_url(url);
if (scroll) scrollTo(scroll.x, scroll.y);
current_history_index = history_index;
return;
}
const delta = history_index - current_history_index;
await navigate({
type: 'popstate',
url,
popped: {
state,
scroll,
delta
},
accept: () => {
current_history_index = history_index;
current_navigation_index = navigation_index;
},
block: () => {
history.go(-delta);
},
nav_token: token,
event
});
} else {
// since popstate event is also emitted when an anchor referencing the same
// document is clicked, we have to check that the router isn't already handling
// the navigation. otherwise we would be updating the page store twice.
if (!hash_navigating) {
const url = new URL(location.href);
update_url(url);
// if the user edits the hash via the browser URL bar, trigger a full-page
// reload to align with pathname router behavior
if (app.hash) {
location.reload();
}
}
}
});
popstateイベントの処理(ブラウザの前に戻る・先に進む処理)
Shallow Navigation(Sveltekitの同じページ内での状態変更)の判定 必要に応じてnavigate関数を呼び出してページ遷移を実行
code:packages/kit/src/runtime/client/client.js
addEventListener('hashchange', () => {
// if the hashchange happened as a result of clicking on a link,
// we need to update history, otherwise we have to leave it alone
if (hash_navigating) {
hash_navigating = false;
history.replaceState(
{
...history.state,
},
'',
location.href
);
}
});
code:packages/kit/src/runtime/client/client.js
// We track the scroll position associated with each history entry in sessionStorage,
// rather than on history.state itself, because when navigation is driven by
// popstate it's too late to update the scroll position associated with the
// state we're navigating from
/**
* history index -> { x, y }
* @type {Record<number, { x: number; y: number }>}
*/
const scroll_positions = storage.get(SCROLL_KEY) ?? {};
/**
* navigation index -> any
* @type {Record<string, any[]>}
*/
const snapshots = storage.get(SNAPSHOT_KEY) ?? {};
リンククリックのインターセプトがいろいろなケースを含めている
code:js
修飾キー(Ctrl、Metaなど)の検出
code:js
if (event.button || event.which !== 1) return;
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
if (event.defaultPrevented) return;
code:js
const { url, external, target, download } = get_link_info(a, base, app.hash);
code:js
if (target === '_parent' || target === '_top') {
if (window.parent !== window) return;
} else if (target && target !== '_self') {
return;
}
urlがmailto:やtel:、myapp:などのプロトコル判定
code:js
// Ignore URL protocols that differ to the current one and are not http(s) (e.g. mailto:, tel:, myapp:, etc.)
// This may be wrong when the protocol is x: and the link goes to y:.. which should be treated as an external
// navigation, but it's not clear how to handle that case and it's not likely to come up in practice.
// MEMO: Without this condition, firefox will open mailer twice.
// See:
if (
!is_svg_a_element &&
url.protocol !== location.protocol &&
!(url.protocol === 'https:' || url.protocol === 'http:')
)
return;
download属性のとき
code:js
if (download) return;
code:js
const nonhash, hash = (app.hash ? url.hash.replace(/^#/, '') : url.href).split('#'); const same_pathname = nonhash === strip_hash(location);
code:js
// Check if new url only differs by hash and use the browser default behavior in that case
// This will ensure the hashchange event is fired
// Removing the hash does a full page navigation in the browser, so make sure a hash is present
if (hash !== undefined && same_pathname) {
// If we are trying to navigate to the same hash, we should only
// attempt to scroll to that element and avoid any history changes.
// Otherwise, this can cause Firefox to incorrectly assign a null
// history state value without any signal that we can detect.
const current_hash = current.url.href.split('#');
if (current_hash === hash) {
event.preventDefault();
// We're already on /# and click on a link that goes to /#, or we're on
// /#top and click on a link that goes to /#top. In those cases just go to
// the top of the page, and avoid a history change.
if (hash === '' || (hash === 'top' && a.ownerDocument.getElementById('top') === null)) {
scrollTo({ top: 0 });
} else {
const element = a.ownerDocument.getElementById(decodeURIComponent(hash));
if (element) {
element.scrollIntoView();
element.focus();
}
}
return;
}
// set this flag to distinguish between navigations triggered by
// clicking a hash link and those triggered by popstate
hash_navigating = true;
update_scroll_positions(current_history_index);
update_url(url);
if (!options.replace_state) return;
// hashchange event shouldn't occur if the router is replacing state.
hash_navigating = false;
}
code:js
event.preventDefault();
// allow the browser to repaint before navigating —
// this prevents INP scores being penalised
await new Promise((fulfil) => {
requestAnimationFrame(() => {
setTimeout(fulfil, 0);
});
setTimeout(fulfil, 100); // fallback for edge case where rAF doesn't fire because e.g. tab was backgrounded
});
await navigate({
type: 'link',
url,
keepfocus: options.keepfocus,
noscroll: options.noscroll,
replace_state: options.replace_state ?? url.href === location.href,
event
});
code:js
function native_navigation(url, replace = false) {
if (replace) {
location.replace(url.href);
} else {
location.href = url.href;
}
return new Promise(() => {});
}
呼び出し元での後続処理を防ぐ目的
履歴などが差し変わらないための処理かもしれない